import * as lsp from "vscode-languageserver/node";
import { Program, t, util } from "./analysis";
import { provideCodeAction } from "./codeAction";
import { defaultCompletionList, provideCompletion } from "./completion";
import { provideDefinition } from "./definition";
import { provideDocumentHighlight } from "./documentHighlight";
import { provideDocumentSymbol } from "./documentSymbol";
import { provideHover } from "./hover";
import { proviedReferences } from "./reference";
import { providePrepareRenameRequest, provideRenameRequest } from "./rename";
import { provideSignatureHelp } from "./signatureHelp";
import { pathToUri, uriToPath } from "./translation";
import { reportDiagnostics } from "./validate";
import { ProjectWorkspace } from "./workspace";
import { TextDocument } from "vscode-languageserver-textdocument";
import { provideFoldingRanges } from "./foldingRanges";
import { provideDocumentSemanticTokens } from "./semanticTokens";

export interface ProjectInitializeParam {
    rootPath: string;
    program?: t.Program;
    locked?: boolean;
    logger?: (message: string) => void;
}

export interface RequestWorkspaceParam {
    workspace: ProjectWorkspace;
    textDocument?: TextDocument;
}

export interface RequestDiagnosticsParam extends RequestWorkspaceParam {
    fsPath: string;
    requestReferenceFiles: boolean;
}

export interface RequestDiagnosticsResult {
    uri: string;
    diagnostics: lsp.Diagnostic[];
}

export class Project {

    public readonly program: t.Program;
    public readonly locked: boolean;
    public readonly rootPath: string;

    private _logEmitter = new lsp.Emitter<string>();
    public readonly onLog = this._logEmitter.event;

    private _errorEmitter = new lsp.Emitter<string>();
    public readonly onError = this._errorEmitter.event;

    public constructor(param: ProjectInitializeParam) {
        this.rootPath = param.rootPath;
        this._logEmitter = new lsp.Emitter<string>();
        if (param.logger) {
            this._logEmitter.event(param.logger);
        }
        if (param.program) {
            this.program = param.program;
        }
        else {
            this.program = new Program();
            this.program.onLog(this.log.bind(this));
            this.program.onError(this.showErrorMessage.bind(this));
            this.program.load(this.rootPath);
        }
        this.locked = param.locked === undefined ? false : param.locked;
    }

    private log(message: string) {
        this._logEmitter.fire(message);
    }

    private showErrorMessage(message: string) {
        this._errorEmitter.fire(message);
    }

    private async doCapabilities<T>(params: lsp.TextDocumentPositionParams, workspace: ProjectWorkspace, defaultValue: T, callback: (source: t.SourceFile, positon: number, ...args: any[]) => Promise<T>, ...args: any[]) {
        let fsPath = uriToPath(params.textDocument.uri);
        let source = this.program.getSourceFile(fsPath);
        let document = workspace.getTextDocument(fsPath);
        if (source && document) {
            return await callback(source, document.offsetAt(params.position), ...args);
        }
        return defaultValue;
    }

    public async provideCompletion(params: lsp.CompletionParams, workspace: ProjectWorkspace): Promise<lsp.CompletionList> {
        return this.doCapabilities(params, workspace, defaultCompletionList, provideCompletion, params.context);
    }

    public async provideHover(params: lsp.HoverParams, workspace: ProjectWorkspace): Promise<lsp.Hover | null> {
        return this.doCapabilities(params, workspace, null, provideHover);
    }

    public async provideDefinition(params: lsp.TextDocumentPositionParams, workspace: ProjectWorkspace): Promise<lsp.Definition | lsp.DefinitionLink[] | null> {
        return this.doCapabilities(params, workspace, null, provideDefinition);
    }

    public async provideReferences(params: lsp.ReferenceParams, workspace: ProjectWorkspace): Promise<lsp.Location[] | null> {
        return this.doCapabilities(params, workspace, null, proviedReferences);
    }

    public async provideSignatureHelp(params: lsp.SignatureHelpParams, workspace: ProjectWorkspace): Promise<lsp.SignatureHelp | null> {
        return this.doCapabilities(params, workspace, null, provideSignatureHelp);
    }

    public async provideCodeAction(params: lsp.CodeActionParams): Promise<lsp.CodeAction[]> {
        return provideCodeAction(params.context, params.textDocument.uri);
    }

    public async provideRenameRequest(params: lsp.RenameParams, workspace: ProjectWorkspace): Promise<lsp.WorkspaceEdit | null> {
        return this.doCapabilities(params, workspace, null, provideRenameRequest, workspace, params.newName);
    }

    public async providePrepareRenameRequest(params: lsp.PrepareRenameParams, workspace: ProjectWorkspace): Promise<lsp.Range | undefined | null> {
        return this.doCapabilities(params, workspace, null, providePrepareRenameRequest);
    }

    public async provideDocumentHighlight(params: lsp.DocumentHighlightParams, workspace: ProjectWorkspace): Promise<lsp.DocumentHighlight[] | undefined> {
        return this.doCapabilities(params, workspace, undefined, provideDocumentHighlight);
    }

    public async provideDocumentSymbol(params: lsp.DocumentSymbolParams): Promise<lsp.DocumentSymbol[] | undefined> {
        let source = this.program.getSourceFile(uriToPath(params.textDocument.uri));
        if (source) {
            return provideDocumentSymbol(source);
        }
        return undefined;
    }

    public async provideDiagnostics(param: RequestDiagnosticsParam): Promise<RequestDiagnosticsResult[]>    {
        let document = param.textDocument ?? param.workspace.getTextDocument(param.fsPath);
        if (!document) {
            return [{
                uri: pathToUri(param.fsPath),
                diagnostics: [],
            }];
        }
        // ignore .mdd file
        if (param.fsPath.toLowerCase().endsWith(".mdd")) {
            return [{
                uri: pathToUri(param.fsPath),
                diagnostics: [],
            }];
        }
        let file = this.program.getFile(param.fsPath);
        if (!file) {
            return [{
                uri: pathToUri(param.fsPath),
                diagnostics: [],
            }];
        }
        if (!this.locked && !file.tracker.getSourceFile()) {
            // this.program.update(path, document.getText());
            try {
                this.program.parse(param.fsPath);
            }
            catch(err) {
                if (err instanceof Error) {
                    this.log(err.message);
                }
                else {
                    throw err;
                }
            }
        }
        let source = this.program.getSourceFile(param.fsPath);
        if (source) {
            let result: RequestDiagnosticsResult[] = [];
            result.push({
                uri: pathToUri(param.fsPath),
                diagnostics: await reportDiagnostics(source.diagnostics, document),
            });
            if (param.requestReferenceFiles) {
                util.forEachSourceFileInclude(source, async s => {
                    let d = param.workspace.getTextDocument(pathToUri(s.fileName));
                    if (d) {
                        result.push({
                            uri: pathToUri(s.fileName),
                            diagnostics: await reportDiagnostics(s.diagnostics, d),
                        });
                    }
                });
            }
            return result;
        }
        return [];
    }

    public async provideFoldingRanges(params: lsp.FoldingRangeParams): Promise<lsp.FoldingRange[] | undefined> {
        let source = this.program.getSourceFile(uriToPath(params.textDocument.uri));
        if (source) {
            return provideFoldingRanges(source);
        }
        return undefined;
    }

    public async provideSemanticTokens(params: lsp.SemanticTokensParams): Promise<lsp.SemanticTokens> {
        let source = this.program.getSourceFile(uriToPath(params.textDocument.uri));
        if (source) {
            return provideDocumentSemanticTokens(source);
        }
        return { data: [] };
    }

    public async provideSemanticTokensRange(param: lsp.SemanticTokensRangeParams, workspace: ProjectWorkspace): Promise<lsp.SemanticTokens> {
        let fsPath = uriToPath(param.textDocument.uri);
        let source = this.program.getSourceFile(fsPath);
        let document = workspace.getTextDocument(fsPath);
        if (source && document) {
            return provideDocumentSemanticTokens(
                source,
                {
                    start: document.offsetAt(param.range.start),
                    end: document.offsetAt(param.range.end)
                }
            );
        }
        return { data: [] };
    }

    public dispose() {
        this.program.clear();
    }

}
